//---------------------------------------------------------------------
//  This file is part of the Background Motion solution.
// 
//  Copyright (C) Mindscape (TM).  All rights reserved.
//  http://www.mindscape.co.nz
// 
//  THIS CODE AND INFORMATION ARE PROVIDED AS IS WITHOUT WARRANTY OF ANY
//  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
//  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
//  PARTICULAR PURPOSE.
//---------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.IO;

using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.QueryParsers;
using Lucene.Net.Search;

using Mindscape.BackgroundMotion.Core.Model;
using Mindscape.BackgroundMotion.Core.Utilities;

namespace Mindscape.BackgroundMotion.Core.Search
{
  /// <summary>
  /// A search engine based on Lucene.NET which implements the Background Motion search engine contract
  /// </summary>
  internal class LuceneSearchEngine : ISearchEngine
  {
    private string _indexDirectory = Path.GetTempPath();

    public LuceneSearchEngine()
    {
      Setup();
    }

    /// <summary>
    /// Searches the index based on the specified query.
    /// </summary>
    /// <param name="query">The query.</param>
    /// <param name="pageSize">Size of the page.</param>
    /// <param name="page">The page.</param>
    /// <param name="totalResults">The total results.</param>
    /// <returns></returns>
    public IList<T> Search<T>(string query, int pageSize, int page, out int totalResults)
      where T : class, IIndexable
    {
      totalResults = 0;

      if (IndexIsInitialised && !string.IsNullOrEmpty(query))
      {
        query = EscapeQuery(query);

        IndexReader reader = IndexReader.Open(_indexDirectory);
        Searcher searcher = new IndexSearcher(reader);
        QueryParser parser = new QueryParser("all", new StandardAnalyzer());
        Query parsedQuery = parser.Parse(query);

        try
        {
          Hits hits = searcher.Search(parsedQuery);

          totalResults = hits.Length();
          int start = (page - 1) * pageSize;
          int end = Math.Min(hits.Length(), (page * pageSize));

          int[] ids = new int[end - start];

          for (int i = start; i < end; i++)
          {
            ids[i - start] = Convert.ToInt32(hits.Doc(i).Get("id"));
          }

          if (ids.Length > 0)
          {
            using (UnitOfWork.Begin())
            {
              return Repository<T>.FindAll(LinqContainsPredicateBuilder.Build<T, int>(ids, "Id"));
            }
          }
        }
        finally
        {
          searcher.Close();
          reader.Close();
        }
      }

      return new List<T>();
    }

    /// <summary>
    /// Adds a document into the index
    /// </summary>
    public void Index(IIndexable item)
    {
      Remove(item);

      IndexWriter writer = new IndexWriter(_indexDirectory, new StandardAnalyzer(), !IndexIsInitialised);

      try
      {
        Document document = new Document();
        document.Add(new Field("id", item.Id.ToString(), Field.Store.YES, Field.Index.UN_TOKENIZED));
        document.Add(new Field("all", item.SearchData, Field.Store.YES, Field.Index.TOKENIZED));

        writer.AddDocument(document);
        writer.Optimize();
      }
      finally
      {
        writer.Close();
      }
    }

    /// <summary>
    /// Removes a item from the index if it exists
    /// </summary>
    /// <param name="item">The item to remove</param>
    public void Remove(IIdentifiable item)
    {
      // segments must exist for the index to exist
      if (IndexIsInitialised)
      {
        IndexReader r = IndexReader.Open(_indexDirectory);

        try
        {
          r.DeleteDocuments(new Term("id", item.Id.ToString()));
        }
        finally
        {
          r.Close();
        }
      }
    }

    /// <summary>
    /// Rebuilds the index as new with all data from the system
    /// </summary>
    public void RebuildIndex<T>()
      where T : class, IIndexable
    {
      IndexWriter writer = new IndexWriter(_indexDirectory, new StandardAnalyzer(), true);

      try
      {
        using (UnitOfWork.Begin())
        {
          foreach (T item in Repository<T>.FindAll())
          {
            Document document = new Document();
            document.Add(new Field("id", item.Id.ToString(), Field.Store.YES, Field.Index.UN_TOKENIZED));
            document.Add(new Field("all", item.SearchData, Field.Store.YES, Field.Index.TOKENIZED));

            writer.AddDocument(document);
          }
        }

        writer.Optimize();
      }
      finally
      {
        writer.Close();
      }
    }

    /// <summary>
    /// Initialises the environment for the search if it's not been created before
    /// </summary>
    private void Setup()
    {
      if (!Directory.Exists(_indexDirectory + "\\BM"))
      {
        Directory.CreateDirectory(_indexDirectory + "\\BM");
        // TODO: Would be nice to call RebuildIndex<Contribution>() now so the environment is ready
      }

      _indexDirectory += "\\BM";
    }

    /// <summary>
    /// Determines if the index exists on disk
    /// </summary>
    private bool IndexIsInitialised
    {
      get { return File.Exists(string.Format("{0}\\segments", _indexDirectory)); }
    }

    /// <summary>
    /// Escapes the query so that lucene can handle it nicely.
    /// </summary>
    /// <param name="query">The query to escape</param>
    /// <returns>A fully escaped lucene query</returns>
    private static string EscapeQuery(string query)
    {
      query = query.Replace(@"\", @"\\");
      query = query.Replace("+", @"\+");
      query = query.Replace("&", @"\&");
      query = query.Replace("|", @"\|");
      query = query.Replace("!", @"\!");
      query = query.Replace("(", @"\(");
      query = query.Replace(")", @"\)");
      query = query.Replace("{", @"\}");
      query = query.Replace("}", @"\}");
      query = query.Replace("[", @"\[");
      query = query.Replace("]", @"\]");
      query = query.Replace("^", @"\^");
      query = query.Replace("\"", @"\""");
      query = query.Replace("~", @"\~");
      query = query.Replace("*", @"\*");
      query = query.Replace("?", @"\?");
      query = query.Replace(":", @"\:");
      query = query.Replace("-", @"\-");

      return query;
    }
  }
}